Knowing a programming language doesn’t make you a software developer. Absolutely every software developer who works with any object oriented language need to be familiar with SOLID principles.
For those who are new to programming and doesn’t yet know what those principles are, SOLID is an acronym, which stands for the following:
- Single responsibility principle
- Open-closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
I have already covered four of these principles in my previous articles. This time, I will explain the final one of them – dependency inversion principle.
As before, C# is the language I will demonstrate it in. However, if you are using any other object oriented language, this article will still be of benefit to you. It’s especially true if your primary language is Java.
So, let’s begin.
What is dependency inversion principle
Dependency inversion principle states that a higher-level object should never depend on a concrete implementation of a lower-level object. Both should depend on abstractions. But what does it actually mean, you may ask?
Any object oriented language will have a way of specifying a contract to which any concrete class or module should adhere. Usually, this is known as interface.
Interface is something that defines the signature of all the public members that the class must have, but, unlike a class, an interface doesn’t have any logic inside of those members. It doesn’t even allow you to define a method body to put the logic in.
But as well as being a contract that defines the accessible surface area of a class, an interface can be used as a data type in variables and parameters. When used in such a way, it can accept an instance of absolutely any class that implements the interface.
And this is where dependency inversion comes from. Instead of passing a concrete class into your methods and constructors, you pass the interface that the class in question implements.
The class that accepts an interface as its dependency is higher level class than the dependency. And passing interface is done because your higher level class doesn’t really care what logic will be executed inside of its dependency if any given method is called on the dependency. All it cares about is that a method with a specific name and signature exists inside the dependency.
Why dependency inversion principle is important
We will take our code from where we left it in the previous article about interface segregation principle.
So, we have the TextProcessor class that modifies input text by converting it into HTML paragraphs:
using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace TextToHtmlConvertor { public class TextProcessor : ITextProcessor { private const string openingParagraphTag = "<p>"; private const string closingParagraphTag = "</p>"; public virtual string ConvertText(string inputText) { var paragraphs = Regex.Split(inputText, @"(\r\n?|\n)") .Where(p => p.Any(char.IsLetterOrDigit)); var sb = new StringBuilder(); foreach (var paragraph in paragraphs) { if (paragraph.Length == 0) continue; sb.AppendLine(openingParagraphTag + paragraph + closingParagraphTag); } sb.AppendLine("<br/>"); return sb.ToString(); } } }
It implements the following interface:
namespace TextToHtmlConvertor { public interface ITextProcessor { string ConvertText(string inputText); } }
We have a more advanced version of TextProcessor that also converts MD tags into corresponding HTML elements. It’s called MdTextProcessor and it’s derived from the original TextProcessor.
using System.Collections.Generic; namespace TextToHtmlConvertor { public class MdTextProcessor : TextProcessor, IMdTextProcessor { private readonly Dictionary<string, (string, string)> tagsToReplace; public MdTextProcessor(Dictionary<string, (string, string)> tagsToReplace) { this.tagsToReplace = tagsToReplace; } public string ConvertMdText(string inputText) { var processedText = base.ConvertText(inputText); foreach (var key in tagsToReplace.Keys) { var replacementTags = tagsToReplace[key]; if (CountStringOccurrences(processedText, key) % 2 == 0) processedText = ApplyTagReplacement(processedText, key, replacementTags.Item1, replacementTags.Item1); } return processedText; } private int CountStringOccurrences(string text, string pattern) { int count = 0; int currentIndex = 0; while ((currentIndex = text.IndexOf(pattern, currentIndex)) != -1) { currentIndex += pattern.Length; count++; } return count; } private string ApplyTagReplacement(string text, string inputTag, string outputOpeningTag, string outputClosingTag) { int count = 0; int currentIndex = 0; while ((currentIndex = text.IndexOf(inputTag, currentIndex)) != -1) { count++; if (count % 2 != 0) { var prepend = outputOpeningTag; text = text.Insert(currentIndex, prepend); currentIndex += prepend.Length + inputTag.Length; } else { var append = outputClosingTag; text = text.Insert(currentIndex, append); currentIndex += append.Length + inputTag.Length; } } return text.Replace(inputTag, string.Empty); } } }
It implements the following interface:
namespace TextToHtmlConvertor { public interface IMdTextProcessor : ITextProcessor { string ConvertMdText(string inputText); } }
We have FileProcessor class that manages files:
using System.IO; namespace TextToHtmlConvertor { public class FileProcessor : IFileProcessor { private readonly string fullFilePath; public FileProcessor(string fullFilePath) { this.fullFilePath = fullFilePath; } public string ReadAllText() { return System.Web.HttpUtility.HtmlEncode(File.ReadAllText(fullFilePath)); } public void WriteToFile(string text) { var outputFilePath = Path.GetDirectoryName(fullFilePath) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(fullFilePath) + ".html"; using (StreamWriter file = new StreamWriter(outputFilePath)) { file.Write(text); } } } }
It implements the following interface:
namespace TextToHtmlConvertor { public interface IFileProcessor { string ReadAllText(); void WriteToFile(string text); } }
And we have Program class that coordinates the entire logic:
using System; using System.Collections.Generic; namespace TextToHtmlConvertor { class Program { static void Main() { try { Console.WriteLine("Please specify the file to convert to HTML."); var fullFilePath = Console.ReadLine(); var fileProcessor = new FileProcessor(fullFilePath); var tagsToReplace = new Dictionary<string, (string, string)> { { "**", ("<strong>", "</strong>") }, { "*", ("<em>", "</em>") }, { "~~", ("<del>", "</del>") } }; var textProcessor = new MdTextProcessor(tagsToReplace); var inputText = fileProcessor.ReadAllText(); var outputText = textProcessor.ConvertMdText(inputText); fileProcessor.WriteToFile(outputText); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } }
Having all the logic inside the Program class is probably not the best way of doing things. It’s meant to be purely an entry point for the application. Since it’s a console application, providing input from the console and output to it is also acceptable. However, having it to coordinate text conversion logic between separate classes is probably not something we want to do.
So, we have moved our logic into a separate class that coordinates the text conversion process and we called it TextConversionCoordinator:
using System; namespace TextToHtmlConvertor { public class TextConversionCoordinator { private readonly FileProcessor fileProcessor; private readonly MdTextProcessor textProcessor; public TextConversionCoordinator(FileProcessor fileProcessor, MdTextProcessor textProcessor) { this.fileProcessor = fileProcessor; this.textProcessor = textProcessor; } public ConversionStatus ConvertText() { var status = new ConversionStatus(); string inputText; try { inputText = fileProcessor.ReadAllText(); status.TextExtractedFromFile = true; } catch (Exception ex) { status.Errors.Add(ex.Message); return status; } string outputText; try { outputText = textProcessor.ConvertMdText(inputText); if (outputText != inputText) status.TextConverted = true; } catch (Exception ex) { status.Errors.Add(ex.Message); return status; } try { fileProcessor.WriteToFile(outputText); status.OutputFileSaved = true; } catch (Exception ex) { status.Errors.Add(ex.Message); return status; } return status; } } }
It has a single method and returns conversation status object, so we can see which parts of the process have succeeded:
using System.Collections.Generic; namespace TextToHtmlConvertor { public class ConversionStatus { public bool TextExtractedFromFile { get; set; } public bool TextConverted { get; set; } public bool OutputFileSaved { get; set; } public List<string> Errors { get; set; } = new List<string>(); } }
And our Program class becomes this:
using System; using System.Collections.Generic; namespace TextToHtmlConvertor { class Program { static void Main() { try { Console.WriteLine("Please specify the file to convert to HTML."); var fullFilePath = Console.ReadLine(); var fileProcessor = new FileProcessor(fullFilePath); var tagsToReplace = new Dictionary<string, (string, string)> { { "**", ("<strong>", "</strong>") }, { "*", ("<em>", "</em>") }, { "~~", ("<del>", "</del>") } }; var textProcessor = new MdTextProcessor(tagsToReplace); var coordinator = new TextConversionCoordinator(fileProcessor, textProcessor); var status = coordinator.ConvertText(); Console.WriteLine($"Text extracted from file: {status.TextExtractedFromFile}"); Console.WriteLine($"Text converted: {status.TextConverted}"); Console.WriteLine($"Output file saved: {status.OutputFileSaved}"); if (status.Errors.Count > 0) { Console.WriteLine("The following errors occured during the conversion:"); Console.WriteLine(string.Empty); foreach (var error in status.Errors) Console.WriteLine(error); } } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } }
Currently, this will work, but it’s almost impossible to write unit tests against it. You won’t just be unit-testing the method. If you run it, you will run your entire application logic.
And you depend on a specific file to be actually present in a specific location. Otherwise, you won’t be able to emulate the successful scenario reliably. The folder structure on different environments where your tests run will be different. And you don’t want to ship some test files with your repository and run some environment-specific setup and tear-down scripts. Remember that those output files need to be managed to?
Luckily, your TextConversionCoordinator class doesn’t care where the file comes from. All it cares about is that fileProcessor returns some text that can be then passed to MdTextProcessor. And it doesn’t care how exactly MdTextProcessor does it’s conversion. All it cares about that the conversion have happened, so it can set the right values in the status object it’s about to return.
Whenever you are unit-testing a method, there are two things you primarily are concerned about:
- Whether the method produces expected outputs based on known inputs.
- Whether all the expected methods on the dependencies were called
And both of these can be easily established if we change the constructor parameters to accept the concrete implementations.
So, we change TextConversionCoordinator class as follows:
using System; namespace TextToHtmlConvertor { public class TextConversionCoordinator { private readonly IFileProcessor fileProcessor; private readonly IMdTextProcessor textProcessor; public TextConversionCoordinator(IFileProcessor fileProcessor, IMdTextProcessor textProcessor) { this.fileProcessor = fileProcessor; this.textProcessor = textProcessor; } public ConversionStatus ConvertText() { var status = new ConversionStatus(); string inputText; try { inputText = fileProcessor.ReadAllText(); status.TextExtractedFromFile = true; } catch (Exception ex) { status.Errors.Add(ex.Message); return status; } string outputText; try { outputText = textProcessor.ConvertMdText(inputText); if (outputText != inputText) status.TextConverted = true; } catch (Exception ex) { status.Errors.Add(ex.Message); return status; } try { fileProcessor.WriteToFile(outputText); status.OutputFileSaved = true; } catch (Exception ex) { status.Errors.Add(ex.Message); return status; } return status; } } }
If you now compile and run the program, it will work in exactly the same way as it did before. However, now we can write unit test for it very easily.
The interfaces can be mocked up, so the methods on them return expected values. And this will enable us to test the logic of the method in isolation from any other components:
using Moq; using System; using System.Linq; using TextToHtmlConvertor; using Xunit; namespace TextToHtmlConvertorTests { public class TextConversionCoordinatorTests { private readonly TextConversionCoordinator coordinator; private readonly Mock<IFileProcessor> fileProcessorMoq; private readonly Mock<IMdTextProcessor> textProcessorMoq; public TextConversionCoordinatorTests() { fileProcessorMoq = new Mock<IFileProcessor>(); textProcessorMoq = new Mock<IMdTextProcessor>(); coordinator = new TextConversionCoordinator(fileProcessorMoq.Object, textProcessorMoq.Object); } // This is a scenario that tests TextConversionCoordinator under the normal circumstances. // The dependency methods have been set up for successful conversion. [Fact] public void CanProcessText() { fileProcessorMoq.Setup(p => p.ReadAllText()).Returns("input"); textProcessorMoq.Setup(p => p.ConvertMdText("input")).Returns("altered input"); var status = coordinator.ConvertText(); Assert.True(status.TextExtractedFromFile); Assert.True(status.TextConverted); Assert.True(status.OutputFileSaved); Assert.Empty(status.Errors); } // This is a scenario that tests TextConversionCoordinator where the text hasn't been changed. // The dependency methods have been set up accordingly. [Fact] public void CanDetectUnconvertedText() { fileProcessorMoq.Setup(p => p.ReadAllText()).Returns("input"); textProcessorMoq.Setup(p => p.ConvertMdText("input")).Returns("input"); var status = coordinator.ConvertText(); Assert.True(status.TextExtractedFromFile); Assert.False(status.TextConverted); Assert.True(status.OutputFileSaved); Assert.Empty(status.Errors); } // This is a scenario that tests TextConversionCoordinator where the text hasn't been read. // The dependency methods have been set up accordingly. [Fact] public void CanDetectUnsuccessfulRead() { fileProcessorMoq.Setup(p => p.ReadAllText()).Throws(new Exception("Read error occurred.")); var status = coordinator.ConvertText(); Assert.False(status.TextExtractedFromFile); Assert.False(status.TextConverted); Assert.False(status.OutputFileSaved); Assert.Single(status.Errors); Assert.Equal("Read error occurred.", status.Errors.First()); } // This is a scenario that tests TextConversionCoordinator where an attempt to convert the text thrown an error. // The dependency methods have been set up accordingly. [Fact] public void CanDetectUnsuccessfulConvert() { fileProcessorMoq.Setup(p => p.ReadAllText()).Returns("input"); textProcessorMoq.Setup(p => p.ConvertMdText("input")).Throws(new Exception("Convert error occurred.")); var status = coordinator.ConvertText(); Assert.True(status.TextExtractedFromFile); Assert.False(status.TextConverted); Assert.False(status.OutputFileSaved); Assert.Single(status.Errors); Assert.Equal("Convert error occurred.", status.Errors.First()); } // This is a scenario that tests TextConversionCoordinator where an attempt to save the file thrown an error. // The dependency methods have been set up accordingly. [Fact] public void CanDetectUnsuccessfulSave() { fileProcessorMoq.Setup(p => p.ReadAllText()).Returns("input"); textProcessorMoq.Setup(p => p.ConvertMdText("input")).Returns("altered input"); fileProcessorMoq.Setup(p => p.WriteToFile("altered input")).Throws(new Exception("Unable to save file.")); var status = coordinator.ConvertText(); Assert.True(status.TextExtractedFromFile); Assert.True(status.TextConverted); Assert.False(status.OutputFileSaved); Assert.Single(status.Errors); Assert.Equal("Unable to save file.", status.Errors.First()); } } }
What we are doing here is checking whether the outputs are as expected and whether specific methods on specific dependencies are being called. I have used Moq NuGet package to verify the latter. This is also the library that allowed me to mock interfaces and set up return values from the methods.
And due to dependency inversion, we were able to write unit tests to literally cover every possible scenario of what TextConversionCoordinator can do.
Dependency inversion is not only useful in unit tests
Although I have given unit tests as an example of why dependency inversion principle is important, the importance of this principle goes far beyond unit tests.
In our working program, we could pass an implementation of IFileProcessor that, instead of reading a file on the disk, reads a file from a web location. After all, TextConversionCoordinator doesn’t care which file the text was extracted from, only that the text was extracted.
So, dependency inversion will add the flexibility to our program. If the external logic changes, TextConversionCoordinator will be able to handle it just the same. No changes will need to be applied to this class.
The opposite of dependency inversion principle is tight coupling. And when this occurs, the flexibility in your program will disappear. This is why tight coupling must be avoided.